package tool.parser
 
import tool.lexer.Lexer
 
import scala.io.Source
 
/** LL parser for the Tool grammar. */
trait Parser extends Lexer {
	self: Compiler =>

	import Trees._
	import tool.lexer.Tokens._

	def parseSource: Tree = {
		readToken // initializes the parser by calling the method in the Lexer.
		val tree: Tree = parseGoal
		terminateIfErrors
		tree
	}

	/** Store the current token, as read from the lexer. */
	private var currentToken: Token = Token(BAD)
	
	def readToken: Unit = {
		currentToken = nextToken
	}
 
	/** ''Eats'' the expected token, or terminates with an error. */
	private def eat(tokenClass : TokenClass): Unit = {
		if(currentToken.tokenClass.isInstanceOf[tokenClass.type])
			readToken
		else
			expected(tokenClass)
	}

	/** Complains that what was found was not expected. The method accepts arbitrarily many arguments of type TokenClass */
	private def expected(tokenClass: TokenClass, more: TokenClass*): Nothing = {
		fatalError("expected: " + (tokenClass::more.toList).mkString(" or ") + ", found: " + currentToken, currentToken)
	}

	private def parseGoal: Tree = {
		val t = Program(parseObject, parseClasses).setPos(currentToken)
		eat(EOF)
		t
	}

	private def parseObject: MainObject = {
		eat(OBJECT)
		val id = parseIdentifier
		eat(LCBRACK)
		eat(DEF)
		eat(MAIN)
		eat(LPAR)
		eat(RPAR)
		eat(COLON)
		eat(UNIT)
		eat(ASSIGN)
		eat(LCBRACK)
		val stat = parseStatement
		eat(RCBRACK)
		eat(RCBRACK)
		MainObject(id, stat).setPos(id)
	}

	private def parseClasses: List[Class] = currentToken.tokenClass match{
		case CLASS => parseClass :: parseClasses
		case _ => Nil
	}

	// parse a class, "helper" method of the parseClasses method
	private def parseClass: Class = currentToken.tokenClass match{
		case CLASS => 
			eat(CLASS); val id = parseIdentifier;
			currentToken.tokenClass match{
				case EXTENDS => {readToken; val ex = parseIdentifier; eat(LCBRACK); val l = parseVarDefs; val i = parseMethDefs; eat(RCBRACK); 
					return ClassWithExtends(id, ex, l, i).setPos(id)}
				case LCBRACK => {readToken; val l = parseVarDefs; val i = parseMethDefs; eat(RCBRACK); return ClassDecl(id, l, i).setPos(id)}
				case _ => expected(EXTENDS, LCBRACK)
			}
		case _ => expected(CLASS)
	}

	private def parseVarDefs: List[VarDecl] = currentToken.tokenClass match{
		case VAR => parseVarDef :: parseVarDefs
		case _ => Nil
	}

	private def parseVarDef: VarDecl = {
		eat(VAR)
		val id = parseIdentifier
		eat(COLON)
		val t = parseType
		eat(SEMICOLUMN)
		VarDecl(id, t).setPos(id)
	}

	private def parseMethDefs: List[MethDecl] = currentToken.tokenClass match{
		case DEF => parseMethDef :: parseMethDefs
		case _ => Nil
	}

	private def parseArgsMethodCall: List[ExprTree] = {
		if(currentToken.tokenClass == RPAR)
			return Nil

		var args = List(parseExpr)
		while(currentToken.tokenClass == COMA){
			eat(COMA)
			args += parseExpr
		}
		args
	}

	private def parseArg: Argument = {
		val id = parseIdentifier; eat(COLON)
		val t = parseType
		Argument(id, t).setPos(id)
	}

	private def parseArgsMethodDef: List[Argument] = {
		if(currentToken.tokenClass == RPAR)
			return Nil
	    
	    var args = List(parseArg)

		while(currentToken.tokenClass == COMA){
			eat(COMA)
			args += parseArg
		}
		args
	}

	private def parseMethDef: MethDecl = {
		eat(DEF)
		val id = parseIdentifier
		eat(LPAR)
		val args: List[Argument] = parseArgsMethodDef; eat(RPAR); eat(COLON)

		val returnType = parseType
  
        val exceptionThrown = currentToken.tokenClass match {
            case THROWS => eat(THROWS); parseThrowsException
            case _ => Nil
        }
        
		eat(ASSIGN); eat(LCBRACK);

		val varDecls: List[VarDecl] = parseVarDefs
		val block = parseBlock(RETURN)
		val returnExpr = parseExpr; eat(SEMICOLUMN); eat(RCBRACK)

        MethDecl(id, args, returnType, exceptionThrown,varDecls, block, returnExpr).setPos(id)
	}
 
    private def parseThrowsException: List[Identifier] = {
        val exceptionId = parseIdentifier
        if(currentToken.tokenClass == COMA) {
            eat(COMA)
			return exceptionId::parseThrowsException
        } 
        
        return exceptionId::Nil 
    }

	private def parseInstanciation: ExprTree = currentToken.tokenClass match{
		case NEW => 
			readToken;
			currentToken.info match{
				case ID(v) => {val t = Instanciation(parseIdentifier).setPos(currentToken); eat(LPAR); eat(RPAR); t}
				case INT => {val p = currentToken; readToken; eat(LBRACK); val t = parseExpr; eat(RBRACK); ArrayCreation(t).setPos(p) }
				case _ => expected(IDCLASS,INT)
			}
		case _ => expected(NEW)
	}

	private def parseType: TypeTree = currentToken.info match{
		case ID(value) => val t = Identifier(value).setPos(currentToken); eat(IDCLASS); t

		case BOOL => val t = BoolType.setPos(currentToken); eat(BOOL); t
		case STRING => val t = StringType.setPos(currentToken); eat(STRING); t

		case INT => 
			val pos = currentToken
			eat(INT)
			if(currentToken.tokenClass == LBRACK){
				eat(LBRACK)
				if(currentToken.tokenClass == RBRACK){
					eat(RBRACK); ArrayType.setPos(pos)
				}else
					expected(RBRACK)
			}else{ 
				IntType.setPos(pos)
			}

		case _ => expected(IDCLASS, BOOL, STRING, INT)
	}
 
    private def parseExceptionType: Identifier = currentToken.info match {
        case ID(value) => val t = Identifier(value).setPos(currentToken); eat(IDCLASS); t
        case _ => expected(IDCLASS)                                                                               
    }

	private def parseAtomicExpr: ExprTree = currentToken.info match{
		case TRUE => {val t = BoolLit(true).setPos(currentToken); eat(TRUE); t}
		case FALSE => {val t = BoolLit(false).setPos(currentToken); eat(FALSE); t}
		case THIS => {val t = Identifier("this").setPos(currentToken); eat(THIS); t}
		case LPAR => parseParExpr
		case STRING(s) => {val t =  StringLit(s).setPos(currentToken); readToken; t}
		case INTLITERAL(v) => {val t = IntLit(v).setPos(currentToken); readToken; t}
		case NEW => parseInstanciation
		case ID(v) => parseIdentifier
		case _ => expected(TRUE, FALSE, THIS, LPAR, STRINGCLASS, INTLITERALCLASS, NEW, IDCLASS)
	}

	private def parseParExpr = currentToken.tokenClass match{
		case LPAR => {eat(LPAR); val t = parseExpr; eat(RPAR); t}
		case _ => expected(LPAR)
	}

	private def parseExpr: ExprTree = parseOr

	private def parseNot: ExprTree = currentToken.tokenClass match{
		case NOT => val pos = currentToken; eat(NOT); Not(parseNot).setPos(pos)
		case _ => parseSpecialChars(parseAtomicExpr)
	}

	private def parseMul: ExprTree = {
		var tree: ExprTree = parseNot
		while(currentToken.tokenClass == TIMES || currentToken.tokenClass == DIV){
			currentToken.tokenClass match{
				case TIMES => val pos = currentToken; eat(TIMES); tree = Times(tree, parseNot).setPos(pos)
				case DIV => val pos = currentToken; eat(DIV); tree = Div(tree, parseNot).setPos(pos)
				case _ => expected(TIMES, DIV)
			}
		}
		tree
	}

	private def parseAdd: ExprTree = {
		var tree: ExprTree = parseMul
		val pos = currentToken
		while(currentToken.tokenClass == PLUS || currentToken.tokenClass == MINUS){
			currentToken.tokenClass match{
				case PLUS => eat(PLUS); tree = Plus(tree, parseMul).setPos(pos)
				case MINUS => eat(MINUS); tree = Minus(tree, parseMul).setPos(pos)
				case _ => expected(PLUS, MINUS)
			}
		}
		tree
	}

	private def parseTest: ExprTree = {
		var tree = parseAdd
		val pos = currentToken
		while(currentToken.tokenClass == EQUALS || currentToken.tokenClass == LESSTHAN){
			currentToken.tokenClass match{
				case EQUALS => eat(EQUALS); tree = Equals(tree, parseAdd).setPos(pos)
				case LESSTHAN => eat(LESSTHAN); tree = LessThan(tree, parseAdd).setPos(pos)
				case _ => expected(EQUALS, LESSTHAN)
			}
		}
		tree
	}

	private def parseAnd: ExprTree = {
		var tree = parseTest
		val pos = currentToken
		while(currentToken.tokenClass == AND)
			currentToken.tokenClass match{
				case AND => eat(AND); tree = And(tree, parseTest).setPos(pos)
				case _ => expected(AND)
			}
		tree
	}

	private def parseOr: ExprTree = {
		var tree = parseAnd
		val pos = currentToken
		while(currentToken.tokenClass == OR)
			currentToken.tokenClass match{
				case OR => eat(OR); tree = Or(tree, parseAnd).setPos(pos)
				case _ => expected(OR)
			}
		tree
	}

	private def parseSpecialChars(base: ExprTree): ExprTree = currentToken.tokenClass match{
		case LBRACK => 
			eat(LBRACK); val pos = currentToken; val expr = parseExpr
			eat(RBRACK)
			parseSpecialChars(ArrayCell(base, expr).setPos(pos))
		case DOT => 
			eat(DOT);
			val pos = currentToken
			currentToken.info match{
				case ID(v) => 
					val id = parseIdentifier; eat(LPAR)
					val args = parseArgsMethodCall; eat(RPAR);
					parseSpecialChars(MethodCall(base, id, args).setPos(pos))
				case ARRAYLENGTH => eat(ARRAYLENGTH); Length(base).setPos(pos)
				case _ => expected(IDCLASS, ARRAYLENGTH)
			}
		case _ => base
	}

	private def parseTryCatchBlock : TryCatchBlock = {
		val pos = currentToken
		eat(TRY)
        eat(LCBRACK);

		(parseBlock(RCBRACK), parseCatchStatements, parseFinallyStatement) match{
			case (Block(Nil), _, _) => error("Empty try", pos); null
			case (_, Nil, None) => error("Try without catch or finally", pos); null
			case l @ _ => TryCatchBlock(l._1, l._2, l._3)
		}
	}
 
    private def parseCatchStatements : List[Catch] = currentToken.tokenClass match {
        case CATCH => parseCatchStatement :: parseCatchStatements
		case _ => Nil
    }
    
    private def parseCatchStatement : Catch = {
        eat(CATCH)
        eat(LPAR)
		val pos = currentToken
        val exceptionId = parseIdentifier
        eat(COLON)
		val exceptionType = parseExceptionType
        eat(RPAR)
        eat(LCBRACK)
        
        Catch(exceptionId, exceptionType, parseBlock(RCBRACK)).setPos(pos)
    }
    
    private def parseFinallyStatement : Option[Finally] = {
        if (currentToken.tokenClass == FINALLY) {
          eat(FINALLY)
          val pos = currentToken
          eat(LCBRACK)
		  parseBlock(RCBRACK) match{
			  case Block(Nil) => error("Empty finally", pos); Some(null)
			  case b @ _ => Some(Finally(b).setPos(pos))
		  }
        }else
			None
    }
    
    private def parseThrowCreation : Throw = {
        eat(THROW)
		val pos = currentToken
        val expr = parseExpr
        eat(SEMICOLUMN)
        Throw(expr).setPos(pos)
    }

	private def parseStatement: StatTree = currentToken.tokenClass match {
		case LCBRACK => eat(LCBRACK); parseBlock(RCBRACK)
		case IF => parseIf
		case WHILE => parseWhile
		case PRINTLN => parsePrintln
		case TRY => parseTryCatchBlock
        case THROW => parseThrowCreation
		case IDCLASS => 
			val id = parseIdentifier
			val pos = currentToken
			currentToken.tokenClass match{
				case LBRACK => 
					eat(LBRACK); val idx = parseExpr; eat(RBRACK); eat(ASSIGN); 
					val expr = parseExpr; eat(SEMICOLUMN); 
					AssignArrayCell(id, idx, expr).setPos(pos)
				case as @ ASSIGN => {readToken; val expr = parseExpr; eat(SEMICOLUMN); Assign(id, expr).setPos(pos)}
				case _ => expected(ASSIGN, LBRACK)
			}
		case _ => expected(TRY, LBRACK, IF, WHILE, PRINTLN, IDCLASS, THROW) 
	}

	private def parseWhile: StatTree = currentToken.tokenClass match{
		case WHILE => { eat(WHILE); val pos = currentToken; eat(LPAR); val expr = parseExpr; eat(RPAR); 
			val stat = parseStatement; While(expr, stat).setPos(pos)}
		case _ => expected(WHILE)
	}

	private def parsePrintln: StatTree = currentToken.tokenClass match{
		case PRINTLN => { readToken; val pos = currentToken; eat(LPAR); val expr = parseExpr; eat(RPAR); eat(SEMICOLUMN); Println(expr).setPos(pos)}
		case _ => expected(PRINTLN)
	}

	private def parseBlock(endingToken: TokenClass): Block = {
		if(currentToken.tokenClass.isInstanceOf[endingToken.type]){
		   	eat(endingToken)
			return Block(Nil).setPos(currentToken)
		}
		else
			Block(parseStatement :: (parseBlock(endingToken)).stats ).setPos(currentToken)
	}

	private def parseIf: StatTree = currentToken.tokenClass match{
		case IF => 
			val pos = currentToken
			eat(IF); eat(LPAR)
			val t = parseExpr; eat(RPAR)
			val s = parseStatement
			currentToken.tokenClass match{
				case ELSE => { eat(ELSE); val ret = IfElse(t, s, parseStatement).setPos(pos); ret }
				case _ => If(t, s).setPos(pos)
			}
		case _ => expected(IF)
	}

	private def parseIdentifier: Identifier = currentToken.info match {
		case ID(value) => {
		    val ret = Identifier(value).setPos(currentToken);
		    readToken;
		    ret}
		case _ => expected(IDCLASS)
	}
}
